﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using System.Windows.Forms;
using ClassLibrary;
using ClassLibrary.Communication;
using ClassLibrary.Configuration;
using ClassLibrary.Configuration.WireCloud;

namespace WireCloud
{
    public partial class WireCloud : Form
    {
        /// <summary>
        /// replaces complex wiring system of a network but still provides the same functionality
        /// provides connection between network nodes
        /// </summary>
        private Socket _server;
        private readonly Dictionary<String, Socket> _clientSocketsDictionary = new Dictionary<string, Socket>(); 
        private bool _activeWireCloud = false;
        private Thread _listeningForCall;
        private List<Link> _routingTable = new List<Link>();
        private int _portTcp;
        // ManualResetEvent instances signal completion.
        public static ManualResetEvent AllDone = new ManualResetEvent(false);
        private static readonly ManualResetEvent SendDone = new ManualResetEvent(false);
        private static ManualResetEvent _receiveDone = new ManualResetEvent(false);
        
        /// <summary>
        /// Tworzy obiekt i ładuje tablicę routingową oraz numer portu, na którym chmura 
        /// pracuje z pliku konfiguracyjnego. Wywołuje metodę ListenForCall().
        /// </summary>
        public WireCloud()
        {
            InitializeComponent();
            string path = @"d://WireCloudConfiguration.xml";
            var settings = (WireCloudConfig)Configuration.GetConfiguration(path, typeof(WireCloudConfig));
            _routingTable = settings.Links;
            _portTcp = settings.CloudPort;
            ListenForCall();
        }

        /// <summary>
        /// Tworzymy socket, na którym będzie pracowała chmura. Tworzymy i rozpoczynamy wątek nasłuchiwania
        /// na zgłoszenia klientów.
        /// </summary>
        private void ListenForCall()
        {
            if (!_activeWireCloud)
                try
                {
                    _server = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
                    var ipLocal = new IPEndPoint ( IPAddress.Any ,_portTcp);
                    _server.Bind( ipLocal );
                    _activeWireCloud = true;
                    _listeningForCall = new Thread(Call_Servicing);
                    _listeningForCall.IsBackground = true;
                    _listeningForCall.Start();
                }
                catch
                {
                    AddItemtoListBox("Blad chmury");
                    _activeWireCloud = false;
                }
        }

        /// <summary>
        /// Obsługuje zgłaszających się klientów. Kiedy klient się zgłosi, akceptujemy połączenie i wywołujemy
        /// OnClientConnect, żeby połączyć klienta z chmurą.
        /// </summary>
        private void Call_Servicing()
        {
            try
            {
                _server.Listen(4);
                while (true)
                {
                    // Set the event to nonsignaled state.
                    AllDone.Reset();
                    AddItemtoListBox("Oczekuje na polaczenie");
                    _server.BeginAccept(OnClientConnect, _server);
                    // Wait until a connection is made before continuing.
                    AllDone.WaitOne();
                }
            }
            catch(SocketException se)
            {
                _activeWireCloud = false;
                _server.Close();
                AddItemtoListBox("Polaczenie z chmura zerwane");
                MessageBox.Show(se.Message);
            }
        }

        /// <summary>
        /// Łączenie klienta z serwerem (chmurą). Po połaczeniu rozpoczynamy wątek, który pobiera ID klienta.
        /// </summary>
        /// <param name="asyn"></param>
        public void OnClientConnect(IAsyncResult asyn)
        {
            try
            {
                // Signal the main thread to continue.
                AllDone.Set();
                Socket listener = (Socket)asyn.AsyncState;
                Socket handler = listener.EndAccept(asyn);

                StateObject stateObject = new StateObject() {WorkSocket = handler};
                handler.BeginReceive(stateObject.Buffer, 0, StateObject.BufferSize, SocketFlags.None, OnIdReceived, stateObject);
            }
            catch (ObjectDisposedException)
            {
                System.Diagnostics.Debugger.Log(0, "1", "\n OnClientConnection: Socket has been closed\n");
            }
            catch (SocketException se)
            {
                MessageBox.Show(se.Message);
            }
        }

        /// <summary>
        /// Klasa wykorzystywana do obsługi danych.
        /// </summary>
        public class StateObject
        {
            // Client  socket.
            public Socket WorkSocket { get; set; }
            // Size of receive buffer.
            public const int BufferSize = 1024*8;
            // Receive buffer.
            private byte[] buffer = new byte[BufferSize];
            public string Id { get; set; }

            public byte[] Buffer
            {
                get { return buffer; }
            }
        }

        /// <summary>
        /// Pobiera ID klienta i mapuje ID z Socketem w słowniku.
        /// Zaczyna oczekiwac na dane od klienta.
        /// </summary>
        /// <param name="asyn"></param>
        public void OnIdReceived(IAsyncResult asyn)
        {
            try
            {
                var stateObject = (StateObject)asyn.AsyncState;
                var mSocClient = stateObject.WorkSocket;
                byte[] m_DataBuffer = stateObject.Buffer;

                int iRx = 0;
                iRx = mSocClient.EndReceive(asyn);
                var chars = new char[iRx + 1];
                System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder();
                d.GetChars(m_DataBuffer, 0, iRx, chars, 0);
                var id = new String(chars).TrimEnd('\0');
                _clientSocketsDictionary[id] = mSocClient;

                // modify the state object.
                stateObject.Id = id;
                AddItemtoListBox(id + " connected " + _clientSocketsDictionary.Count);
                // now start to listen for any data...
                mSocClient.BeginReceive(stateObject.Buffer, 0, StateObject.BufferSize, SocketFlags.None, OnDataReceived, stateObject);
            }
            catch (ObjectDisposedException)
            {
                System.Diagnostics.Debugger.Log(0, "1", "\nOnDataReceived: Socket has been closed\n");
            }
            catch (SocketException se)
            {
                MessageBox.Show(se.Message);
            }
        }

        /// <summary>
        /// Obsługuje odebrane dane. Deserializuje otrzymany obiekt i sprawdza w tablicy routingowej gdzie
        /// obiekt ma być dalej przesłany. Jeżeli odnaleziono łącze, na które dane mają być wysłane
        /// zmienia numer portu obiektu na port docelowy i wysyła dane za pomocą metody send()
        /// W przeciwnym przypadku wyświetli brak łącza lub docelowego węzła.
        /// Następnie spowrotem oczekuje na dane.
        /// </summary>
        /// <param name="asyn"></param>
        public void OnDataReceived(IAsyncResult asyn)
        {
            StateObject state = (StateObject)asyn.AsyncState;
            if (true) ;
            try
            {
                Socket handler = state.WorkSocket;
                // Read data from the client socket. 
                int bytesRead = handler.EndReceive(asyn);
                BinaryFormatter formatter = new BinaryFormatter();
                MemoryStream memorystream = new MemoryStream(state.Buffer, 0, bytesRead);

                IndexedData _data = (IndexedData)formatter.Deserialize(memorystream);

                Link.EndPoint endPoint = Check_Routing_Table(state.Id, _data.PortId);
                
                if (endPoint == null)
                {
                    AddItemtoListView("???", "???", -1, -1);
                    handler.BeginReceive(state.Buffer, 0, StateObject.BufferSize, SocketFlags.None, OnDataReceived, state);
                    return;
                }
                    

                var portOut = endPoint.PortId;
                var iData = Change_port_id(_data, portOut);

                string idOut = endPoint.Id;
                Socket client;
                
                if (_clientSocketsDictionary.TryGetValue(idOut, out client))
                {
                    AddItemtoListView(state.Id,
                                      endPoint.Id,
                                      _data.PortId,
                                      endPoint.PortId);

                    Send(client, iData);
                }
                else
                {
                    AddItemtoListView("XXX", "XXX", -1, -1);
                }
                // now start to listen for any data...
                handler.BeginReceive(state.Buffer, 0, StateObject.BufferSize, SocketFlags.None, OnDataReceived, state);
            }
            catch (ObjectDisposedException)
            {
                System.Diagnostics.Debugger.Log(0, "1", "\nOnDataReceived: Socket has been closed\n");
            }
            catch (SocketException se)
            {
                _clientSocketsDictionary.Remove(state.Id);
                MessageBox.Show(se.Message);
            }
        }

        /// <summary>
        /// Serializacja i wysyłanie otrzymanego obiektu do danego węzła.
        /// </summary>
        /// <param name="client"></param>
        /// <param name="iData"></param>
        public void Send(Socket client, IndexedData iData)
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            MemoryStream memoryStream = new MemoryStream();
            binaryFormatter.Serialize(memoryStream, iData);
            var serializedData = memoryStream.ToArray();
            // Begin sending the data to the remote device.
            client.BeginSend(serializedData, 0, serializedData.Length, SocketFlags.None, new AsyncCallback(SendCallback), client);
            SendDone.WaitOne();
        }

        /// <summary>
        /// Potwierdzenie wysłania danych.
        /// </summary>
        /// <param name="ar"></param>
        private static void SendCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket client = (Socket)ar.AsyncState;

                // Complete sending the data to the remote device.
                int bytesSent = client.EndSend(ar);

                // Signal that all bytes have been sent.
                SendDone.Set();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        /// <summary>
        /// Zmienia numer portu w otrzymanych danych
        /// </summary>
        /// <param name="data"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        public IndexedData Change_port_id(IndexedData data, int id)
        {
            IndexedData iData = new IndexedData(data.Data, id);
            return iData;
        }

        /// <summary>
        /// Przeszukuje tablice routingu.
        /// </summary>
        /// <param name="Id"></param>
        /// <param name="PortIN"></param>
        /// <returns></returns>
        public Link.EndPoint Check_Routing_Table(string Id, int PortIN)
        {
            foreach(Link link in _routingTable)
            {
                var endPoint = link.Try(Id, PortIN);
                if (endPoint != null)
                {
                    return endPoint;
                }


            }
            return null;
        }

        /// <summary>
        /// Ładuje tablicę routingową  z pliku.
        /// </summary>
        /// <param name="FilePath"></param>
        public void Load_Routing_Table(string FilePath)
        {
            var settings = (WireCloudConfig)Configuration.GetConfiguration(@FilePath, typeof(WireCloudConfig));
            _routingTable = settings.Links;
        }

        /// <summary>
        /// Wyświetla komunikat w komponencie ListBox.
        /// </summary>
        /// <param name="one"></param>
        private void AddItemtoListBox(string one)
        {
            this.Invoke((MethodInvoker)delegate
            {
                string Time = DateTime.Now.ToString("[HH:mm:ss tt] ");
                listBox1.Items.Add(Time+one);
            });
        }

        /// <summary>
        /// Wyświetla dane łącza, czyli węzeł źródłowy, docelowy, port żródłowy, docelowy.
        /// </summary>
        /// <param name="SrcName"></param>
        /// <param name="EndName"></param>
        /// <param name="SrcPort"></param>
        /// <param name="NdPort"></param>
        private void AddItemtoListView(string SrcName, string EndName, int SrcPort, int NdPort)
        {
            this.Invoke((MethodInvoker)delegate
            {
                ListViewItem item = new ListViewItem(new[] 
                { 
                    SrcName, 
                    EndName, 
                    SrcPort.ToString(), 
                    NdPort.ToString()
                });
                listView1.Items.Add(item);
            });
        }

        /// <summary>
        /// Ładuje plik konfiguracyjny za pomocą komponentu toolLoad.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void toolLoad_Click(object sender, EventArgs e)
        {
            openFileDialog1.Filter = "XML|*.xml";
            if (openFileDialog1.ShowDialog() == DialogResult.OK)
            {
                string WireCloudDirectory = openFileDialog1.FileName;
                Load_Routing_Table(WireCloudDirectory);
            }

        }
    }
}
